Skip to content

Fix Jailbreak — bind the CR 110.2a "under their control" clause (#6691) - #6814

Merged
matthewevans merged 8 commits into
phase-rs:mainfrom
keloide:claude/phase-developer-track-card-8qz8o1
Aug 1, 2026
Merged

Fix Jailbreak — bind the CR 110.2a "under their control" clause (#6691)#6814
matthewevans merged 8 commits into
phase-rs:mainfrom
keloide:claude/phase-developer-track-card-8qz8o1

Conversation

@keloide

@keloide keloide commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses #6691 for the FilterProp::Owned template and the player_scope fan-out template; does not close it — residue enumerated below.

The parser recognised only "under your control" and the owner-stating forms. "under their control" / "under that player's control" fell through the empty-tag arm of the alternation at oracle_effect/lower.rs:6705 and were silently dropped, so permanents entered under the wrong controller while the cards reported supported: true, gap_count: 0 — a silent wrong-game-result class.

This collapses four independently-implemented copies of the under <possessor> control grammar into one nom combinator module and binds the anaphor from typed parse products only, never a text scan or a lowered-tree walk:

  • N1 — the moved object's own FilterProp::Owned { controller != You }ControllerRef::ParentTargetOwner. Licensed by CR 400.1 + 400.3 + 404.1 (a card in a graveyard is in its owner's graveyard) + CR 108.3.
  • N2ctx.relative_player_scope, mapped through an exhaustive, wildcard-free table admitting only ScopedPlayer, ChosenPlayer, TriggeringPlayer. TargetPlayer/TargetOpponent are refused by name: two incompatible provenances, and TargetPlayer resolves to the first player target, which would put every land under one player's control on a multi-target cast (Turtle Tracks).

Anything unbindable fails closed to Effect::unimplemented, so a clause the parser cannot bind becomes an honest coverage gap instead of a wrong controller. EntersUnderSpec carries that third state — which Option<ControllerRef> cannot express, and whose absence is the bug — through the AST to every lowering site.

Files changed

  • crates/engine/src/parser/oracle_nom/enters_under.rs (new)
  • crates/engine/src/parser/oracle_nom/mod.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/oracle_effect/{lower,mod,imperative,sequence,tests}.rs
  • crates/engine/src/parser/oracle_ir/snapshots/*.snap (2)
  • crates/engine/tests/integration/issue_6691_enters_under_their_control.rs (new), tests/integration/main.rs, tests/fixtures/integration_cards.json

CR references

CR 110.2a @618 (authorizing) · 110.2 @616 · 110.1 @614 · 608.2c @2793 · 608.2k @2814 · 608.2e @2798 · 400.1 @1933 · 400.3 @1937 · 404.1 @2030 · 108.3 @564 · 109.4 @594 · 109.5 @610 · 115.10 @886 · 102.2/102.3 @252/254 · 603.2 @2561 · 614.1c @3060 · 508.4 @2309 · 708.3 @5707

All grep-verified in docs/MagicCompRules.txt before being written; the diff gate returns zero UNVERIFIED.

Measured impact (full-pool diff, not predicted)

Regenerated card-data.json before/after. Exactly 7 cards move; zero others change:

Card before after
Jailbreak (dropped) ParentTargetOwner
Gerrymandering (dropped) ScopedPlayer
Thieves' Auction (dropped) ScopedPlayer
The Beamtown Bullies (silently wrong) honest gap
Endless Whispers (silently wrong) honest gap
Plague Reaver (silently wrong) honest gap
Turtle Tracks (silently wrong) honest gap

933 your/owner-form cards (558+316+27+20+6+6) keep byte-identical behaviour while flowing through a single grammar. The 13 deferred possessor forms become a one-alt-arm extension instead of four parallel edits.

Coverage moves down by 4 by design: those cards were reporting gap_count: 0 on a priority:p2-wrong-game-result clause. An honest gap beats a wrong controller.

Implementation method

Method: /engine-implementer — plan → /review-engine-plan x2 (both returned blockers; both resolved) → implement → /review-impl (returned a blocker; resolved, see Validation Failures).

Track

Developer

LLM

Model: claude-opus-5
Thinking: high

Verification

  • Required checks ran clean.

  • Gate A output below is for the current committed head.

  • Both anchors cite existing analogous code at the same seam.

  • Final review-impl could not be re-run after the last commit — see Validation Failures.

  • cargo fmt --all — clean

  • cargo check -p engine --lib — 0 errors, 0 warnings

  • cargo test -p engine --lib parser::8678 passed; 0 failed

  • cargo test -p engine --test integration4164 passed; 0 failed

  • cargo clippy -p engine --all-targets — no warnings

  • ./scripts/gen-card-data.sh — regenerated; full-pool diff above

  • Manual grep for the ungated blind spots (.rfind/.split/.split_once/.contains() in added parser lines — one hit, a test assertion on a debug dump at tests.rs:29926, not dispatch. No allow-noncombinator added anywhere.

Revert-sensitivity proven, not asserted. Temporarily binding EntersUnderSpec::Default instead of Override(ParentTargetOwner) makes jailbreak_enters_under_their_control_binds_to_the_owner FAIL; restoring makes it pass.

Gate A

Gate A PASS head=2b252930196ab8425c4293a4c7394303575ccb75 base=c754a087a28ce6cabb46ea5c8fa6bfa095e79008

Anchored on

  • crates/engine/src/parser/oracle_effect/lower.rs:6705 — the existing enters_under alt() binding " under your control" -> Some(ControllerRef::You) and returning None for the owner forms. The exact seam the new grammar replaces; it already carried its own CR 110.2a annotation.
  • crates/engine/src/parser/oracle_effect/lower.rs:6641 — the sibling destination-table carrier enters_under: enters_under_you.then_some(ControllerRef::You), showing how the same controller override is threaded through the phrase-table path.

Claimed parse impact

  • Jailbreak
  • Gerrymandering
  • Thieves' Auction
  • The Beamtown Bullies (now an honest gap)
  • Endless Whispers (now an honest gap)
  • Plague Reaver (now an honest gap)
  • Turtle Tracks (now an honest gap)

Residue — why this does not close #6691

  1. Rootweaver Druid stays supported: true, gap_count: 0 with enters_under: "You" on its "and the rest onto the battlefield tapped under their control" leg — parser: under their control binding unparsed — permanents enter under the wrong controller (CR 110.2a) #6691's exact failure mode surviving this fix, because that leg is produced by a dig/reveal path this change does not rewire (sequence.rs:5468/5560/6868/6930).
  2. Immortal Obligation is intercepted upstream by is_static_pattern() at priority 7 (its text contains "can't block") and never reaches these seams — a separate line-classifier defect.
  3. Four cards move to an honest gap rather than a fix (table above).
  4. Un-rewired detection sites remain, named in-source with // FOLLOW-UP comments: the four dig/reveal legs, imperative.rs:390-398, imperative.rs:6440, and oracle_trigger.rs:9597 (the trigger-condition layer, CR 603.2 — deliberately out of scope, and the home of the 7 Trap/condition cards that must not be touched).
  5. 13 deferred possessor forms (under an opponent's control etc.) are explicit NPs, not anaphors, and are unchanged.

Validation Failures

The final review-impl pass could not be re-run against the current head 2b25293. An independent review-impl did run against the preceding head and returned one BLOCKER plus five lower findings; the BLOCKER is fixed in this branch (commit 2 — the integration test passed with the fix reverted, because the fixture entry was stale and the runtime default coincides with the correct answer for Jailbreak). The environment then exhausted its model budget, so the confirming re-review did not run.

Recorded but not fixed, for maintainer triage:

  • (MED) Seam D has no dedicated production-path test. No printed card reaches it with a third-person clause, so no non-synthetic fixture exists.
  • (MED) Fail-closed is not total on the chain-patcher path. apply_search_destination_to_ability_chain is called with None on an unbound anaphor so the destination/tapped patches survive (deliberately — skipping the call would drop them), but a predecessor ChangeZone can still execute with the default controller while only the newly-pushed def becomes Unimplemented.
  • (MED) fold_control_clauses widens the pattern set over an unchanged span. The span is byte-identical to the literal it replaces, but the fold now also matches the anaphor anywhere in it. No printed card currently pairs a conditional "...enter the battlefield under their control this turn" with a battlefield put on the same normalized line, so the guard is incidental rather than structural.
  • (LOW) One annotation cites CR 110.2 where the licensing authority is CR 110.2a plus the engine convention that enters_under: None means "keep current controller".
  • (LOW) Seam-A offset arithmetic maps lower->text byte offsets across a clause containing ' and U+2019. Both are length-preserving under to_lowercase (so the flagged risk is not live), but TextPair would make it structural.

Also not run: cargo coverage, cargo semantic-audit, ordering_parity_sweep, scripts/coverage-regression-check.sh. The full-pool card-data.json diff above is the substantive evidence in their place; predicted bucket parser_regress (non-fatal), net supported delta -4, which CI will confirm.

CI Failures

None observed locally.


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Improved handling of “enters under [possessor]’s control” clauses for battlefield entries and returns.
    • Added support for owner, third-person, and related control wording across zone changes.
  • Bug Fixes
    • Prevented unresolved control references from incorrectly defaulting to the resolving player.
    • Ensured unsupported control clauses fail safely instead of producing unintended moves.
  • Tests
    • Added parser, regression, and integration coverage, including Jailbreak owner-control behavior.

@keloide
keloide requested a review from matthewevans as a code owner July 30, 2026 14:11
@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The parser now recognizes battlefield-entry control clauses, preserves explicit, default, and unbound states, and binds clauses to player context. Zone-change lowering fails closed for unresolved possessors. Parser and integration tests cover control binding and Jailbreak.

Changes

Enters-under control binding

Layer / File(s) Summary
Control clause contract and binding
crates/engine/src/parser/oracle_nom/enters_under.rs, crates/engine/src/parser/oracle_ir/ast.rs
Adds control-clause parsing, antecedent resolution, binding helpers, and the three-state EntersUnderSpec representation.
Return destination control parsing
crates/engine/src/parser/oracle_effect/lower.rs
Stores raw control possessors on destinations and parses battlefield clauses with trailing entry riders.
Zone-change binding and fail-closed lowering
crates/engine/src/parser/oracle_effect/imperative.rs, crates/engine/src/parser/oracle_effect/mod.rs
Propagates bound control specifications through zone changes and emits unimplemented effects for unbound possessors.
Search continuation propagation
crates/engine/src/parser/oracle_effect/sequence.rs, crates/engine/src/parser/oracle_effect/mod.rs
Passes parse context into continuation parsing and binds control clauses before search-chain effect construction.
Controller-binding regression coverage
crates/engine/src/parser/oracle_effect/tests.rs, crates/engine/tests/integration/*
Tests explicit, owner, default, and unbound control outcomes, including a three-player Jailbreak scenario.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CardText
  participant ReturnDestinationParser
  participant ControlClauseBinder
  participant ZoneChangeLowering
  participant GameEffect
  CardText->>ReturnDestinationParser: parse battlefield destination and control clause
  ReturnDestinationParser->>ControlClauseBinder: pass possessor and antecedent context
  ControlClauseBinder->>ZoneChangeLowering: return EntersUnderSpec
  ZoneChangeLowering->>GameEffect: create controller override or unimplemented anaphor effect
Loading

Possibly related PRs

Suggested reviewers: matthewevans

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR fixes the pure-imperative path but explicitly leaves required detection paths, possessor forms, and affected cards unresolved. Complete the remaining #6691 parser paths and explicit possessor forms, or split the unresolved work into a separately scoped issue.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the CR 110.2a controller-binding fix for Jailbreak, matching the primary changes.
Out of Scope Changes check ✅ Passed The changes remain focused on CR 110.2a parsing, AST propagation, fail-closed lowering, and controller-binding regression tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
crates/engine/src/parser/oracle_nom/enters_under.rs (1)

325-354: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The final (p, _) arm defeats the exhaustiveness discipline this module relies on elsewhere.

map_relative_player_scope is deliberately wildcard-free so "adding a ControllerRef variant breaks the build here" (Line 246). bind_control_clause gives up that property: the deferred possessor forms listed at Lines 150-160 ("under an opponent's control", "under target player's control", …) will, the moment they are added as ControlClausePossessor variants, fall silently into UnboundAnaphor rather than forcing a decision at this binding table. Fail-closed is the safe direction, but the card becomes an unexplained coverage gap instead of a compile error.

Match the possessor axis explicitly so the compiler flags a new variant.

As per coding guidelines: "Use exhaustive match expressions without wildcard fallbacks when matching known enums, allowing the compiler to detect missing variants."

♻️ Explicit anaphor arms instead of the tuple wildcard
-        // Fail closed: no nameable antecedent (and, for the demonstrative, an
-        // object-owner antecedent it may not legally use).
-        (p, _) => EntersUnderSpec::UnboundAnaphor(p),
+        // Fail closed: no nameable antecedent (and, for the demonstrative, an
+        // object-owner antecedent it may not legally use). Enumerated so a new
+        // possessor variant breaks the build here rather than becoming a silent
+        // coverage gap.
+        (
+            p @ (ControlClausePossessor::TheirAnaphor
+            | ControlClausePossessor::ThatPlayerDemonstrative),
+            ControlAnaphorAntecedent::MovedObjectOwner | ControlAnaphorAntecedent::Unnameable,
+        ) => EntersUnderSpec::UnboundAnaphor(p),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_nom/enters_under.rs` around lines 325 - 354,
Update bind_control_clause to remove the tuple wildcard `(p, _)` and enumerate
each currently unsupported ControlClausePossessor variant explicitly, returning
UnboundAnaphor for those cases. Preserve the existing You, Owner, TheirAnaphor,
and ThatPlayerDemonstrative behavior while making the match exhaustive so adding
a new possessor variant produces a compile-time error.

Source: Coding guidelines

crates/engine/src/parser/oracle_effect/sequence.rs (1)

5308-5326: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

The searched player's filter is available here and would license the CR 108.3 owner antecedent.

Passing None as moved_object is accurate for this AST shape, but the information is not actually absent: the preceding Effect::SearchLibrary in defs carries the player filter whose library is searched, and for "search target opponent's library … put it onto the battlefield under their control" that player is the found card's owner (CR 400.3 — a card in a library is in its owner's library). As written, such a card can only reach ContextPlayer or fail closed. Not a defect in this PR's scope, but worth a note here (or in the deferred list) so the seam's gap is attributable rather than looking like an inherent limitation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_effect/sequence.rs` around lines 5308 - 5326,
Document this deferred limitation at the bind_control_clause seam: although
moved_object is correctly None for this AST shape, the preceding
Effect::SearchLibrary in defs provides the searched player filter, which can
identify the found card’s owner for CR 108.3. Add a concise note here or to the
deferred-work list explaining that opponent-library searches currently cannot
use this owner antecedent and may resolve only to ContextPlayer or fail closed.
crates/engine/src/parser/oracle_ir/ast.rs (1)

220-271: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Three-state contract is only half-adopted across enters_under fields.

SearchDestination, ReturnToBattlefield, ReturnAllToZone, ZoneChange, and ZoneChangeAll now carry EntersUnderSpec, but the sibling controller-override fields in this same file stay Option<ControllerRef>:

  • ContinuationAst::DigFromAmong.enters_under (Line 406)
  • ContinuationAst::RevealUntilKept.enters_under (Line 469)
  • PutImperativeAst::Manifest.enters_under (Line 1511)

Those three are fed by literal scan_contains(lower, "under your control") checks in oracle_effect/sequence.rs, so a printed "under their control" / "under that player's control" at those seams still degrades to the CR 110.2 default rather than failing closed — exactly the class this PR is closing. Today no printed card appears to reach them with a third-person clause, so this is a coverage/uniformity gap rather than an active misparse; worth either migrating them onto EntersUnderSpec in a follow-up or recording the carve-out in this doc comment so the asymmetry is intentional and discoverable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_ir/ast.rs` around lines 220 - 271, Complete
the three-state contract for the remaining entry-controller fields: change
ContinuationAst::DigFromAmong.enters_under,
ContinuationAst::RevealUntilKept.enters_under, and
PutImperativeAst::Manifest.enters_under from Option<ControllerRef> to
EntersUnderSpec, and update their sequence-lowering/parsing paths to preserve
unbound third-person control clauses as UnboundAnaphor instead of defaulting.
Reuse EntersUnderSpec’s existing helpers and ensure all constructors and
consumers handle Default, Override, and UnboundAnaphor consistently.
crates/engine/src/parser/oracle_effect/tests.rs (1)

29864-29992: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a destination-layer unit test for the third-person control-clause fallback.

jailbreak_enters_under_their_control_binds_to_the_owner and unbindable_anaphor_fails_closed_instead_of_defaulting exercise the new TheirAnaphor/ThatPlayerDemonstrative fallback only through full parse_effect_chain/parse_oracle_text, conflating the destination-parsing layer (strip_return_destination_ext_with_remainder in lower.rs) with the downstream binding layer. A direct test mirroring return_destination_owners_control_not_under_your_control — e.g. asserting strip_return_destination_ext("it to the battlefield under their control").1.unwrap().control == Some(ControlClausePossessor::TheirAnaphor) — would isolate a regression in the new parse_leading_control_clause fallback from a regression in bind_control_clause.

🧪 Suggested additional test
#[test]
fn return_destination_under_their_control_recognized_as_anaphor() {
    let (_, dest) = strip_return_destination_ext("it to the battlefield under their control");
    let d = dest.expect("should parse destination");
    assert_eq!(d.control, Some(ControlClausePossessor::TheirAnaphor));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_effect/tests.rs` around lines 29864 - 29992,
Add a focused destination-layer unit test alongside return-destination tests,
using strip_return_destination_ext (or its remainder variant) to parse “it to
the battlefield under their control” and assert the destination control is
Some(ControlClausePossessor::TheirAnaphor). Keep this test independent of
parse_effect_chain and bind_control_clause so it specifically covers
parse_leading_control_clause’s fallback recognition.
crates/engine/src/parser/oracle_effect/imperative.rs (1)

2331-2336: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the fail-closed guard into one authority on EntersUnderSpec. The identical unbound_possessor()Effect::unimplemented("change_zone_enters_under_anaphor", p.printed_clause()) block, including the gap-name string literal, is repeated at five lowering sites. A single accessor (e.g. EntersUnderSpec::unimplemented_if_unbound(&self) -> Option<Effect>) keeps the gap name and CR 110.2a rationale in one place and makes a future lowering site that forgets the guard obvious.

  • crates/engine/src/parser/oracle_effect/imperative.rs#L2331-L2336: replace with the shared accessor (if let Some(e) = enters_under.unimplemented_if_unbound() { return e; }).
  • crates/engine/src/parser/oracle_effect/imperative.rs#L2396-L2401: same replacement in the ReturnAllToZone arm.
  • crates/engine/src/parser/oracle_effect/imperative.rs#L6762-L6767: same replacement in the PutImperativeAst::ZoneChangeAll arm.
  • crates/engine/src/parser/oracle_effect/imperative.rs#L6796-L6801: same replacement in the PutImperativeAst::ZoneChange arm.
  • crates/engine/src/parser/oracle_effect/imperative.rs#L11635-L11640: same, wrapping the returned effect in parsed_clause(..).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_effect/imperative.rs` around lines 2331 -
2336, Centralize the unbound-possessor fail-closed behavior in an accessor on
EntersUnderSpec, such as unimplemented_if_unbound, preserving the
change_zone_enters_under_anaphor gap name and printed clause. Replace the
duplicated guards at crates/engine/src/parser/oracle_effect/imperative.rs lines
2331-2336, 2396-2401, 6762-6767, and 6796-6801 with the accessor and return its
Effect; at lines 11635-11640, use the accessor and wrap the returned effect in
parsed_clause as currently required.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/parser/oracle_effect/sequence.rs`:
- Around line 3727-3741: Handle the unbound enters_under.possessor() case as a
failed assembly: at
crates/engine/src/parser/oracle_effect/sequence.rs:3727-3741, stop or neutralize
the existing library-to-battlefield ChangeZone in previous before
apply_search_destination_to_ability_chain so no executable wrong-controller move
remains; at crates/engine/src/parser/oracle_effect/sequence.rs:3770-3781, skip
forward_result and Effect::Attach wiring when put_effect is the
Effect::unimplemented fragment, or fold the attach behavior into that fragment.

In `@crates/engine/tests/integration/issue_6691_enters_under_their_control.rs`:
- Around line 59-143: Add a separate integration test using a production card
ability with a named-player controller binding, such as The Beamtown Bullies’
“that player’s control,” where the default controller differs from the
referenced player. Build and resolve the scenario through the normal card
database and game runner pipeline, then assert the permanent enters under the
named player’s control and that the relevant effect does not revert.

---

Nitpick comments:
In `@crates/engine/src/parser/oracle_effect/imperative.rs`:
- Around line 2331-2336: Centralize the unbound-possessor fail-closed behavior
in an accessor on EntersUnderSpec, such as unimplemented_if_unbound, preserving
the change_zone_enters_under_anaphor gap name and printed clause. Replace the
duplicated guards at crates/engine/src/parser/oracle_effect/imperative.rs lines
2331-2336, 2396-2401, 6762-6767, and 6796-6801 with the accessor and return its
Effect; at lines 11635-11640, use the accessor and wrap the returned effect in
parsed_clause as currently required.

In `@crates/engine/src/parser/oracle_effect/sequence.rs`:
- Around line 5308-5326: Document this deferred limitation at the
bind_control_clause seam: although moved_object is correctly None for this AST
shape, the preceding Effect::SearchLibrary in defs provides the searched player
filter, which can identify the found card’s owner for CR 108.3. Add a concise
note here or to the deferred-work list explaining that opponent-library searches
currently cannot use this owner antecedent and may resolve only to ContextPlayer
or fail closed.

In `@crates/engine/src/parser/oracle_effect/tests.rs`:
- Around line 29864-29992: Add a focused destination-layer unit test alongside
return-destination tests, using strip_return_destination_ext (or its remainder
variant) to parse “it to the battlefield under their control” and assert the
destination control is Some(ControlClausePossessor::TheirAnaphor). Keep this
test independent of parse_effect_chain and bind_control_clause so it
specifically covers parse_leading_control_clause’s fallback recognition.

In `@crates/engine/src/parser/oracle_ir/ast.rs`:
- Around line 220-271: Complete the three-state contract for the remaining
entry-controller fields: change ContinuationAst::DigFromAmong.enters_under,
ContinuationAst::RevealUntilKept.enters_under, and
PutImperativeAst::Manifest.enters_under from Option<ControllerRef> to
EntersUnderSpec, and update their sequence-lowering/parsing paths to preserve
unbound third-person control clauses as UnboundAnaphor instead of defaulting.
Reuse EntersUnderSpec’s existing helpers and ensure all constructors and
consumers handle Default, Override, and UnboundAnaphor consistently.

In `@crates/engine/src/parser/oracle_nom/enters_under.rs`:
- Around line 325-354: Update bind_control_clause to remove the tuple wildcard
`(p, _)` and enumerate each currently unsupported ControlClausePossessor variant
explicitly, returning UnboundAnaphor for those cases. Preserve the existing You,
Owner, TheirAnaphor, and ThatPlayerDemonstrative behavior while making the match
exhaustive so adding a new possessor variant produces a compile-time error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 416d9312-f59c-4081-bf6b-12ba184477f1

📥 Commits

Reviewing files that changed from the base of the PR and between fd6c14f and 2b25293.

⛔ Files ignored due to path filters (2)
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__boseiju_who_endures_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__case_of_the_stashed_skeleton_ir.snap is excluded by !**/*.snap, !**/snapshots/**
📒 Files selected for processing (11)
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/oracle_nom/enters_under.rs
  • crates/engine/src/parser/oracle_nom/mod.rs
  • crates/engine/tests/fixtures/integration_cards.json
  • crates/engine/tests/integration/issue_6691_enters_under_their_control.rs
  • crates/engine/tests/integration/main.rs

Comment thread crates/engine/src/parser/oracle_effect/sequence.rs Outdated
@matthewevans

Copy link
Copy Markdown
Member

Maintainer conflict port for a36ca3c7766dc3118fd7fba4cf800a28af17a4ed has been reviewed clean. Required CI is still running on this current head; approval and merge-when-ready enqueue will resume when it settles.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 6 card(s), 8 signature(s) (baseline: main b45555a71b48)

🟢 Added (3 signatures)

  • 1 card · ➕ ability/change_zone_enters_under_anaphor · added: change_zone_enters_under_anaphor
    • Affected (first 3): Plague Reaver
  • 1 card · ➕ ability/change_zone_enters_under_anaphor · added: change_zone_enters_under_anaphor (kind=activated)
    • Affected (first 3): The Beamtown Bullies
  • 1 card · ➕ ability/change_zone_enters_under_anaphor · added: change_zone_enters_under_anaphor (targets=0+)
    • Affected (first 3): Turtle Tracks

🔴 Removed (3 signatures)

  • 1 card · ➖ ability/ChangeZone · removed: ChangeZone (target=self, to=battlefield)
    • Affected (first 3): Plague Reaver
  • 1 card · ➖ ability/SearchLibrary · removed: SearchLibrary (find=basic land, targets=0+)
    • Affected (first 3): Turtle Tracks
  • 1 card · ➖ ability/TargetOnly · removed: TargetOnly (kind=activated, target=opponent)
    • Affected (first 3): The Beamtown Bullies

🟡 Modified fields (2 signatures)

  • 2 cards · 🔄 ability/ChangeZone · changed field enters_under: ScopedPlayer
    • Affected (first 3): Gerrymandering, Thieves' Auction
  • 1 card · 🔄 ability/ChangeZone · changed field enters_under: ParentTargetOwner
    • Affected (first 3): Jailbreak

1 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@matthewevans

Copy link
Copy Markdown
Member

Maintainer CI repair is pushed on current head 126fc0c4b34fb869b901aa8e4e514a571d9fecda: it factors the Clippy-reported parser-table type and refreshes the five deterministic SearchDestination.enters_under snapshots from null to "Default". Required CI is queued on this head; approval and merge-when-ready enqueue remain held until it completes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/engine/src/parser/oracle_effect/lower.rs (2)

6754-6768: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Consume battlefield riders after the control clause.

After parse_leading_control_clause succeeds, this path immediately returns after consuming one space. Valid forms such as to the battlefield under their control face down and tapped therefore leave the riders in the remainder and return face_down: false / enter_tapped: false. The other destination path already scans riders after control; reuse that logic here.
As per path instructions, engine destination parsing must remain composable and rules-correct across rider orderings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_effect/lower.rs` around lines 6754 - 6768,
Update the battlefield return path following parse_leading_control_clause to
consume and apply trailing battlefield riders such as face down and tapped,
including valid rider orderings, before constructing ReturnDestination. Reuse
the existing rider-scanning logic from the other destination path, preserving
the remaining input and setting face_down and enter_tapped correctly.

Source: Path instructions


6639-6662: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve control clauses after enter-with-counters suffixes.

Control parsing only runs before parse_with_counters_suffix_spanned. For valid text such as with two stun counters under their control, the counter parser consumes the prefix, while the trailing control clause is neither bound nor returned; the destination silently falls back to control: None, breaking CR 110.2a behavior. Parse the counter-suffix remainder for a control clause, and add an assertion for dest.control to the existing regression fixture.
As per path instructions, engine parsing must preserve rules-correct control binding across valid clause orderings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_effect/lower.rs` around lines 6639 - 6662,
Update the destination parsing flow around parse_with_counters_suffix_spanned to
inspect its remaining text for a trailing control clause before finalizing the
destination. Use parse_leading_control_clause to bind the parsed player to
dest.control while preserving existing rider and suffix handling for orderings
such as counters followed by control. Extend the existing regression fixture
with an assertion that dest.control is populated.

Source: Path instructions

crates/engine/src/parser/oracle_effect/imperative.rs (1)

2052-2057: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

CR citation mismatch: "CR 110.1" doesn't describe controller ownership.

CR 110.1 defines what a permanent is ("A permanent is a card or token on the battlefield") and says nothing about controllers. The claim "only permanents have a controller" maps to CR 110.2 ("Every permanent has a controller") together with CR 108.4 ("A card doesn't have a controller unless that card represents a permanent or spell"), not CR 110.1.

📝 Suggested annotation fix
-                        // CR 110.1 (docs/MagicCompRules.txt:614): only
-                        // permanents have a controller.
+                        // CR 110.2 + CR 108.4 (docs/MagicCompRules.txt): only
+                        // permanents have a controller.
                         enters_under: EntersUnderSpec::Default,

Same mis-citation recurs at Line 2080-2082 (d.zone variant) — apply the same fix there.

As per path instructions: "rules-touching code with no verified CR <number>: <description> annotation, or a CR citation whose rule body does not describe the code" is a finding, and "119 starting-life / 120 damage / 121 draw are adjacent and confused" flags exactly this class of numeric-citation risk.

Also applies to: 2080-2085

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_effect/imperative.rs` around lines 2052 -
2057, Correct the rules citations in both `EntersUnderSpec::Default`
initializations around the `d.zone` and non-`d.zone` variants: replace the
incorrect CR 110.1 reference with citations to CR 110.2 and CR 108.4, which
establish controller ownership for permanents and the applicable card types.
Keep the existing behavior and annotation wording otherwise unchanged.

Source: Path instructions

🧹 Nitpick comments (1)
crates/engine/src/parser/oracle_effect/imperative.rs (1)

2325-2357: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fail-closed enters_under.unbound_possessor() guard duplicated 5×.

The pattern if let Some(p) = enters_under.unbound_possessor() { return Effect::unimplemented("change_zone_enters_under_anaphor", p.printed_clause()); } followed by enters_under.as_controller_ref() repeats verbatim at Line 2331-2343 (ReturnToBattlefield), Line 2396-2414 (ReturnAllToZone), Line 6802-6819/6843-6849 (lower_put_ast's two arms), and Line 11682-11688 (lower_imperative_family_ast's partition arm). All five call sites are correctly guarded (verified — none is missing the check), but the boilerplate could collapse into one helper, e.g. EntersUnderSpec::resolve_or_unimplemented() -> Result<Option<ControllerRef>, Effect>, callable with ?/match at each site.

♻️ Sketch of the consolidation (illustrative, not exhaustive)
-            if let Some(p) = enters_under.unbound_possessor() {
-                return Effect::unimplemented(
-                    "change_zone_enters_under_anaphor",
-                    p.printed_clause(),
-                );
-            }
-            Effect::ChangeZone {
-                ...
-                enters_under: enters_under.as_controller_ref(),
+            let enters_under = match enters_under.resolve_or_unimplemented("change_zone_enters_under_anaphor") {
+                Ok(v) => v,
+                Err(effect) => return effect,
+            };
+            Effect::ChangeZone {
+                ...
+                enters_under,

Also applies to: 2393-2401, 6787-6849, 11668-11688

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_effect/imperative.rs` around lines 2325 -
2357, Consolidate the repeated fail-closed handling of
enters_under.unbound_possessor() into a single EntersUnderSpec helper that
returns either the resolved controller reference or the existing
Effect::unimplemented result. Update the ReturnToBattlefield, ReturnAllToZone,
both lower_put_ast arms, and the lower_imperative_family_ast partition arm to
use this helper while preserving the current failure key and printed clause.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/parser/oracle_effect/mod.rs`:
- Around line 15812-15817: Update both comments associated with
EntersUnderSpec::Default to cite CR 110.2 instead of CR 110.1, while preserving
the existing wording and behavior.

---

Outside diff comments:
In `@crates/engine/src/parser/oracle_effect/imperative.rs`:
- Around line 2052-2057: Correct the rules citations in both
`EntersUnderSpec::Default` initializations around the `d.zone` and non-`d.zone`
variants: replace the incorrect CR 110.1 reference with citations to CR 110.2
and CR 108.4, which establish controller ownership for permanents and the
applicable card types. Keep the existing behavior and annotation wording
otherwise unchanged.

In `@crates/engine/src/parser/oracle_effect/lower.rs`:
- Around line 6754-6768: Update the battlefield return path following
parse_leading_control_clause to consume and apply trailing battlefield riders
such as face down and tapped, including valid rider orderings, before
constructing ReturnDestination. Reuse the existing rider-scanning logic from the
other destination path, preserving the remaining input and setting face_down and
enter_tapped correctly.
- Around line 6639-6662: Update the destination parsing flow around
parse_with_counters_suffix_spanned to inspect its remaining text for a trailing
control clause before finalizing the destination. Use
parse_leading_control_clause to bind the parsed player to dest.control while
preserving existing rider and suffix handling for orderings such as counters
followed by control. Extend the existing regression fixture with an assertion
that dest.control is populated.

---

Nitpick comments:
In `@crates/engine/src/parser/oracle_effect/imperative.rs`:
- Around line 2325-2357: Consolidate the repeated fail-closed handling of
enters_under.unbound_possessor() into a single EntersUnderSpec helper that
returns either the resolved controller reference or the existing
Effect::unimplemented result. Update the ReturnToBattlefield, ReturnAllToZone,
both lower_put_ast arms, and the lower_imperative_family_ast partition arm to
use this helper while preserving the current failure key and printed clause.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ae892d00-44e6-4cc1-a1ca-88756500c54c

📥 Commits

Reviewing files that changed from the base of the PR and between 2b25293 and 126fc0c.

⛔ Files ignored due to path filters (5)
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__aangs_journey_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__analyze_the_pollen_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__boseiju_who_endures_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__case_of_the_stashed_skeleton_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__stoneforge_mystic_ir.snap is excluded by !**/*.snap, !**/snapshots/**
📒 Files selected for processing (6)
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/tests/fixtures/integration_cards.json
  • crates/engine/tests/integration/main.rs

Comment thread crates/engine/src/parser/oracle_effect/mod.rs
@matthewevans matthewevans self-assigned this Jul 30, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocked — the new fail-closed representation is bypassed on the SearchDestination assembly path, and the current test suite does not exercise that path.

🔴 Blocker

apply_clause_continuation still patches a pre-existing library-to-battlefield ChangeZone with enters_under.as_controller_ref() before it creates the new Effect::unimplemented node for an unbound anaphor. Evidence: crates/engine/src/parser/oracle_effect/sequence.rs:3727-3740 calls apply_search_destination_to_ability_chain with None; its implementation at crates/engine/src/parser/oracle_effect/sequence.rs:5048-5073 leaves the existing controller untouched while still rewriting destination/tapped. The later unbound branch at crates/engine/src/parser/oracle_effect/sequence.rs:3747-3781 only substitutes the newly appended node and still adds forward_result/Attach when present. That leaves an executable default-controller move alongside the reported change_zone_enters_under_anaphor gap, so the parser reports an honest failure while the chain can still perform the wrong move.

The existing failure-closed test does not cover this production branch: crates/engine/src/parser/oracle_effect/tests.rs:30079-30091 tests a direct return (lower_put_ast), while the affected path is the SearchLibrary continuation. The integration test also explicitly documents that it passes with the pre-fix AST at crates/engine/tests/integration/issue_6691_enters_under_their_control.rs:18-37.

Suggested fix: make EntersUnderSpec::UnboundAnaphor abort or replace the entire search-destination assembly before apply_search_destination_to_ability_chain mutates its predecessor, including the attachment continuation. Add a parser/production-path regression that exercises an unbound search-destination clause and proves no executable ChangeZone or attachment survives.

🟡 Non-blocking

bind_control_clause ends with (p, _) at crates/engine/src/parser/oracle_nom/enters_under.rs:353. The module otherwise deliberately uses exhaustive controller mappings; enumerate the remaining anaphor/antecedent combinations so a future ControlClausePossessor cannot silently become an unexplained gap.

The current parse-diff sticky comment reports six affected cards, while the PR body claims seven and names Endless Whispers as an additional honest gap. Reconcile that claim with the current artifact before re-review; no changed signature in the sticky evidence identifies Endless Whispers. CI is green, but it does not resolve the assembly-path defect above.

Recommendation: request changes. Repair the whole SearchDestination fail-closed assembly and cover that exact path before re-review.

@matthewevans matthewevans added the bug Bug fix label Jul 30, 2026
@matthewevans matthewevans removed their assignment Jul 30, 2026
@matthewevans

Copy link
Copy Markdown
Member

Current-head disposition — rebase required before re-review.

I reproduced a textual conflict between this head and current main in crates/engine/src/parser/oracle_effect/imperative.rs. This is not the mechanical integration/main.rs module-order case: main now changes the same return-to-battlefield parser construction that this PR's control-binding/fail-closed work changes. Resolving it safely requires reconciling both parser behaviors and re-validating the CR 110.2a path, so it is not a maintainer-fixup-sized port.

Please rebase this head onto current main, preserve both behaviors, and push the resolved branch. The current head has only triage/security/CodeRabbit statuses; the required Rust/card-data checks have not run for it. The existing requested-changes review remains in force. No approval or merge-queue action has been taken.

@keloide
keloide force-pushed the claude/phase-developer-track-card-8qz8o1 branch 2 times, most recently from 0266dab to dde4f83 Compare July 31, 2026 21:00
@matthewevans matthewevans self-assigned this Jul 31, 2026
@matthewevans

Copy link
Copy Markdown
Member

Current-head review complete — held for external evidence.

The prior SearchDestination blocker is resolved on head dde4f83: sequence.rs now replaces the whole preinstalled search assembly before any default-controller move or attachment can survive, and the added production-path parser regression asserts that no ChangeZone or Attach remains for an unbound clause.

Required Rust lint/tests/card-data checks and the current-head parse-diff artifact are still running; CodeRabbit is also pending. This is a non-terminal hold only: once those current-head results settle, maintainer review will recheck the artifact/reviewer findings and then approve or request changes. No merge-queue action has been taken.

@matthewevans matthewevans removed their assignment Jul 31, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/engine/src/parser/oracle_effect/imperative.rs (1)

12220-12243: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the unbound control clause before attaching the sub-ability.

lower_targeted_action_ast returns Effect::unimplemented when enters_under.unbound_possessor() is Some (line 2340). This arm ignores that outcome and still installs Effect::Attach { attachment: SelfRef, target: host } as clause.sub_ability. For the form "return … to the battlefield under their control attached to <host>", the zone change becomes a coverage gap while the attach still resolves. The source then attaches to the host although the permanent never entered the battlefield.

Every other lowering site in this change guards first — lines 2340, 2405, 6840, 6874, and 11713. Apply the same guard here so the whole clause fails closed.

🛡️ Proposed fail-closed guard
         )) => {
+            // CR 110.2a (docs/MagicCompRules.txt:618): fail closed BEFORE the
+            // Attach sub-ability is built. A gapped ChangeZone paired with a
+            // live Attach would attach the source to the host although the
+            // permanent never entered the battlefield.
+            if let Some(p) = enters_under.unbound_possessor() {
+                return parsed_clause(Effect::unimplemented(
+                    "change_zone_enters_under_anaphor",
+                    p.printed_clause(),
+                ));
+            }
             let mut clause = parsed_clause(lower_targeted_action_ast(
                 TargetedImperativeAst::ReturnToBattlefield {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_effect/imperative.rs` around lines 12220 -
12243, Guard the result of lower_targeted_action_ast in the ReturnToBattlefield
arm before assigning the Attach sub-ability: when
enters_under.unbound_possessor() is Some, preserve the unimplemented clause and
do not install Effect::Attach. Match the existing fail-closed handling used by
the other lowering sites, while retaining the current SelfRef attachment
behavior for bound control clauses.
🧹 Nitpick comments (1)
crates/engine/tests/integration/issue_6691_enters_under_their_control.rs (1)

95-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Match the parsed effect instead of a Debug substring.

The guard formats the whole face with {face:?} and searches for ParentTargetOwner. Any unrelated field or ability that stringifies that variant satisfies the guard, so it can pass for the wrong reason. The guard also does not pin which field carries the binding.

Walk the parsed abilities and match the shape directly, for example Effect::ChangeZone { destination: Zone::Battlefield, enters_under: Some(ControllerRef::ParentTargetOwner), .. }. The guard then fails only for the intended reason and survives a variant rename as a compile error rather than a silent weakening.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/tests/integration/issue_6691_enters_under_their_control.rs`
around lines 95 - 106, Replace the Debug-string assertion in the Jailbreak
fixture guard with a direct match over its parsed abilities, identifying an
Effect::ChangeZone whose destination is Zone::Battlefield and whose enters_under
is Some(ControllerRef::ParentTargetOwner). Keep the assertion scoped to that
intended parsed effect so unrelated fields cannot satisfy it.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/engine/src/parser/oracle_effect/imperative.rs`:
- Around line 12220-12243: Guard the result of lower_targeted_action_ast in the
ReturnToBattlefield arm before assigning the Attach sub-ability: when
enters_under.unbound_possessor() is Some, preserve the unimplemented clause and
do not install Effect::Attach. Match the existing fail-closed handling used by
the other lowering sites, while retaining the current SelfRef attachment
behavior for bound control clauses.

---

Nitpick comments:
In `@crates/engine/tests/integration/issue_6691_enters_under_their_control.rs`:
- Around line 95-106: Replace the Debug-string assertion in the Jailbreak
fixture guard with a direct match over its parsed abilities, identifying an
Effect::ChangeZone whose destination is Zone::Battlefield and whose enters_under
is Some(ControllerRef::ParentTargetOwner). Keep the assertion scoped to that
intended parsed effect so unrelated fields cannot satisfy it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 53f719dd-ab49-4e0a-b946-ee1476ee9d19

📥 Commits

Reviewing files that changed from the base of the PR and between 011896a and dde4f83.

⛔ Files ignored due to path filters (5)
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__aangs_journey_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__analyze_the_pollen_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__boseiju_who_endures_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__case_of_the_stashed_skeleton_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__stoneforge_mystic_ir.snap is excluded by !**/*.snap, !**/snapshots/**
📒 Files selected for processing (11)
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/oracle_nom/enters_under.rs
  • crates/engine/src/parser/oracle_nom/mod.rs
  • crates/engine/tests/fixtures/integration_cards.json
  • crates/engine/tests/integration/issue_6691_enters_under_their_control.rs
  • crates/engine/tests/integration/main.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • crates/engine/src/parser/oracle_nom/mod.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/oracle_nom/enters_under.rs

@matthewevans

Copy link
Copy Markdown
Member

Current-head maintainer hold for 5e6dd887c8fa14bfa7b3c0a762b7c562733e0288: this is not a renewed design blocker. GitHub has just merged current main into the branch, so the prior check run and parser-diff sticky do not establish this merge head's result.

Please let the current Rust/card-data CI and a current-head <!-- coverage-parse-diff --> artifact settle. A fresh implementation review on that evidence will then determine approval; no approval or merge-queue action occurs during this hold.

@matthewevans

Copy link
Copy Markdown
Member

Maintainer follow-up on current head 5ce504525c5e2e867a99aa9f3ab28b352b52b3c4: the remaining fail-closed hole in a direct return … under their control attached to … lowering is fixed. The lowering now returns change_zone_enters_under_anaphor before constructing Attach, and a targeted parser regression proves no child attachment remains. All prior CI applied to 6268cfb9a42b0fbf6223fa320d5b29d5b0a56a61; fresh Rust/card-data/parse-diff CI is required for this new parser head before approval or queueing.

@matthewevans matthewevans removed their assignment Aug 1, 2026
@matthewevans

Copy link
Copy Markdown
Member

Correction to the abbreviated/full SHA in the prior hold: the current head is 5ce504525c62077fa75c91b206051010b1a89253. The stated guard and regression are on that exact commit; fresh CI remains required.

claude and others added 8 commits August 1, 2026 18:55
…s#6691)

The Oracle parser recognised only "under your control" and the
owner-stating forms. "under their control" / "under that player's
control" fell through the empty-tag arm and were silently dropped, so
permanents entered under the wrong controller while the cards reported
supported: true, gap_count: 0 — a silent wrong-game-result class.

Collapse four independently-implemented copies of the
`under <possessor> control` grammar into one nom combinator module
(oracle_nom/enters_under.rs) and bind the anaphor from typed parse
products only:

  N1 — the moved object's own FilterProp::Owned { controller != You }
       (CR 400.1 + 400.3 + 404.1 + 108.3: a card in a graveyard is in
       its owner's graveyard) -> ControllerRef::ParentTargetOwner
  N2 — ctx.relative_player_scope, mapped through an exhaustive
       wildcard-free table that admits only ScopedPlayer, ChosenPlayer
       and TriggeringPlayer

Everything else fails closed to Effect::unimplemented rather than
silently defaulting, so a clause the parser cannot bind becomes an
honest coverage gap instead of a wrong controller. EntersUnderSpec
carries that third state through the AST to every lowering site.

Fixes Jailbreak (ParentTargetOwner) and Gerrymandering (ScopedPlayer)
and their templates; 933 "your"/owner-form cards keep byte-identical
behaviour through the single grammar.

Addresses phase-rs#6691; does not close it — see the PR body for the residue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Nr9Cg6eBZfBod5Twb6Vyi
…h + scope correction

Review found the integration test passed with the fix reverted. Two causes,
both fixed here:

1. The surgically-added `jailbreak` fixture entry was copied from a PRE-FIX
   `card-data.json`, so it carried no `enters_under` key. Since the field is
   `skip_serializing_if = "Option::is_none"`, it deserialized as the unfixed
   AST and the test measured old behaviour while appearing to pass. The entry
   is regenerated from post-fix card data (+0 keys, only `jailbreak` changed)
   and a fixture-integrity guard now fails loudly if it ever goes stale again.

2. The test claimed to be revert-failing. It is not, and that was MEASURED, not
   assumed: with `enters_under` stripped it still passes, because a graveyard →
   battlefield move already resolves the new object's controller to its OWNER,
   which for Jailbreak is exactly the player the clause names. Staging a
   divergent controller does not separate them either — the move reassigns it.
   The docstring now states this plainly and points at the discriminating test.

The discriminating test is the parser-level assertion
`jailbreak_enters_under_their_control_binds_to_the_owner`, verified by
temporarily binding `Default` instead of `ParentTargetOwner`: it FAILS on
revert and passes when restored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Nr9Cg6eBZfBod5Twb6Vyi
@keloide
keloide force-pushed the claude/phase-developer-track-card-8qz8o1 branch from 5ce5045 to 29bc00e Compare August 1, 2026 19:00
@matthewevans matthewevans self-assigned this Aug 1, 2026
@matthewevans

Copy link
Copy Markdown
Member

Current-head hold for 29bc00eb2184d7aa685a8b6c4b45636cc8af5f91.

Fresh implementation review is clean for the formerly blocked paths: unbound control clauses now replace the complete SearchDestination assembly (rather than leaving a move or attachment live), and the direct attached-return path fails before an Attach sub-ability is constructed. The final regression also proves the bindable sibling still reaches the real SearchLibrary → ChangeZone → Attach assembly.

This is not an approval yet. Rust lint and both Rust test shards remain in progress, and the only parse-diff artifact is timestamped 2026-08-01T17:03:25Z, before this head's current CI began. Please wait for those current-head results and a refreshed parse artifact; no auto-merge or merge-queue action is enabled during the hold.

@matthewevans matthewevans removed their assignment Aug 1, 2026
@keloide
keloide requested a review from matthewevans August 1, 2026 19:29
@matthewevans matthewevans self-assigned this Aug 1, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved for the current head: the typed enters_under authority, complete fail-closed SearchDestination/attached-return handling, card-level parse delta, and discriminating runtime coverage have been re-reviewed clean.

@matthewevans
matthewevans added this pull request to the merge queue Aug 1, 2026
@matthewevans matthewevans removed their assignment Aug 1, 2026
Merged via the queue into phase-rs:main with commit 1ea2bc1 Aug 1, 2026
14 checks passed
@keloide
keloide deleted the claude/phase-developer-track-card-8qz8o1 branch August 1, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

parser: under their control binding unparsed — permanents enter under the wrong controller (CR 110.2a)

3 participants